Repository navigation
feat(spo2): surface WHOOP 5.0 spo2_candidate_82 behind experimental toggle (#103) - #1185
Merged
Merged
Conversation
…oggle (#103) WHOOP 5.0/MG does not write spo2Sample (the ratio-of-ratios SpO₂ path is 4.0-only); it emits a strap-computed candidate at byte @82 of the v18 historical record. This surfaces that nightly `spo2_candidate_82` mean in the Blood Oxygen tile as a "strap estimate (unverified)" fallback when no calibrated spo2Pct exists, behind a DEFAULT-OFF experimental toggle (Settings → Diagnostics). Display-only and rule-compliant (CLAUDE.md derived-biosignal): the value is written to metricSeries as "spo2_candidate" under the "-noop" computed ID, NEVER to spo2Pct, and never feeds recovery or illness scoring. The @82 candidate has SPLIT cross-device evidence (+0.99 over 8 nights, but 2 nights opposite on the original device — device/ firmware variance unresolved), so it ships off-by-default and labelled unverified. Uses the pre-existing, parity-tested AnalyticsEngine.nightlySpo2CandidateMean helper. Rebased onto current main + fixed to land cleanly: - HealthScreen.kt: hoisted `NoopPrefs.spo2CandidateDisplay(LocalContext.current)` out of the `remember {}` calculation lambda — a @composable read there is a hard compile error (the reason the PR's Android build was red). - Baselined the experimental toggle's title + blurb on both platforms in i18n_audit_baseline.json (deferred localization for experimental copy). Co-authored-by: evoveotech <evoveotech@users.noreply.github.com>
This was referenced Aug 9, 2026
ryanbr
added a commit
that referenced
this pull request
Aug 9, 2026
…#103 follow-up) (#1186) Follow-up to #1185. Two Android nits found on re-review: - The Blood Oxygen tile's candidate captions in HealthVitalsLogic were RAW string literals ("Estimate (unverified)" / "toggle ON · no @82 data") — hardcoded English that the i18n audit doesn't catch (it misses literals returned from logic-file functions, not just @composable Text). Extracted both to strings.xml via uiString and translated across the shipped locales (de/es/fr/pt-PT/zh), matching iOS which already used String(localized:). - Wording parity: Android said "Estimate (unverified)"; iOS says "strap estimate (unverified)". Aligned Android to the iOS wording so the tile reads the same. Also collect spo2CandidateByDay reactively in HealthScreen (collectAsStateWithLifecycle instead of .value), matching VitalSignsScreen, so the Latest-readings card recomposes on its own when the candidate map updates rather than only incidentally via `days`. Android-only, display-only, no behavior/scoring change. compileFullDebugKotlin + i18n_audit --ci both green.
simoncad7
pushed a commit
to simoncad7/noop
that referenced
this pull request
Aug 17, 2026
…oggle (ryanbr#103) (ryanbr#1185) WHOOP 5.0/MG does not write spo2Sample (the ratio-of-ratios SpO₂ path is 4.0-only); it emits a strap-computed candidate at byte @82 of the v18 historical record. This surfaces that nightly `spo2_candidate_82` mean in the Blood Oxygen tile as a "strap estimate (unverified)" fallback when no calibrated spo2Pct exists, behind a DEFAULT-OFF experimental toggle (Settings → Diagnostics). Display-only and rule-compliant (CLAUDE.md derived-biosignal): the value is written to metricSeries as "spo2_candidate" under the "-noop" computed ID, NEVER to spo2Pct, and never feeds recovery or illness scoring. The @82 candidate has SPLIT cross-device evidence (+0.99 over 8 nights, but 2 nights opposite on the original device — device/ firmware variance unresolved), so it ships off-by-default and labelled unverified. Uses the pre-existing, parity-tested AnalyticsEngine.nightlySpo2CandidateMean helper. Rebased onto current main + fixed to land cleanly: - HealthScreen.kt: hoisted `NoopPrefs.spo2CandidateDisplay(LocalContext.current)` out of the `remember {}` calculation lambda — a @composable read there is a hard compile error (the reason the PR's Android build was red). - Baselined the experimental toggle's title + blurb on both platforms in i18n_audit_baseline.json (deferred localization for experimental copy). Co-authored-by: evoveotech <evoveotech@users.noreply.github.com>
simoncad7
pushed a commit
to simoncad7/noop
that referenced
this pull request
Aug 17, 2026
…ryanbr#103 follow-up) (ryanbr#1186) Follow-up to ryanbr#1185. Two Android nits found on re-review: - The Blood Oxygen tile's candidate captions in HealthVitalsLogic were RAW string literals ("Estimate (unverified)" / "toggle ON · no @82 data") — hardcoded English that the i18n audit doesn't catch (it misses literals returned from logic-file functions, not just @composable Text). Extracted both to strings.xml via uiString and translated across the shipped locales (de/es/fr/pt-PT/zh), matching iOS which already used String(localized:). - Wording parity: Android said "Estimate (unverified)"; iOS says "strap estimate (unverified)". Aligned Android to the iOS wording so the tile reads the same. Also collect spo2CandidateByDay reactively in HealthScreen (collectAsStateWithLifecycle instead of .value), matching VitalSignsScreen, so the Latest-readings card recomposes on its own when the candidate map updates rather than only incidentally via `days`. Android-only, display-only, no behavior/scoring change. compileFullDebugKotlin + i18n_audit --ci both green.
DX23876
added a commit
to DX23876/noop
that referenced
this pull request
Sep 26, 2026
The previous commit said SpO2 comes from neither strap and the v18 byte-82 value is never shown. That is wrong: since ryanbr#1185 (2026-08-09) the default-off Settings toggle "Experimental · Blood Oxygen" surfaces its nightly mean in the Blood Oxygen tile as "strap estimate (unverified)". A 4.0 has no such byte. It still never feeds scoring, and the NOOP Limitations grid's "No" predates the toggle by a day. Analysis migration required: no (documentation only).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Rebase of #1180 by @evoveotech onto current
main, with the two blockers fixed so it lands clean.What it does
WHOOP 5.0/MG doesn't write
spo2Sample(ratio-of-ratios is 4.0-only); it emits a strap-computed candidate at byte @82 of the v18 record. This surfaces that nightlyspo2_candidate_82mean in the Blood Oxygen tile as a "strap estimate (unverified)" fallback when no calibratedspo2Pctexists, behind a default-off experimental toggle (Settings → Diagnostics), with a "toggle ON · no @82 data" diagnostic when the stream is empty.Rule-compliant (the reason this is mergeable where #1173's SpO₂ half wasn't)
metricSeries["spo2_candidate"]under the-noopcomputed ID, neverspo2Pct, and never feeds recovery/illness scoring.AnalyticsEngine.nightlySpo2CandidateMean; the newmetricSeriespersistence mirrors byte-identically on both platforms.Fixes I made to land it
HealthScreen.ktcalledNoopPrefs.spo2CandidateDisplay(LocalContext.current)inside aremember {}calculation lambda (LocalContext.currentis a@Composableread → hard compile error; this is why the PR'sbuild-and-testwas red). Hoisted it into the composable body and keyedrememberon the resolved value.i18n_audit_baseline.json. Deferred localization is standard for experimental diagnostic copy that may not survive the experiment.Verification
./gradlew compileFullDebugKotlin✅ (fixes the red build)i18n_audit.py --ci main→ exit 0 ✅Credit @evoveotech (#1180). Closes #103.